import {CondsMap} from "./frm.js";

let elementMeta = {}

function getMeta(ctx) {
    let name = ''
    if (ctx.constructor) {
        name = ctx.constructor.name
    }
    return  elementMeta[name]
}

export function createUUID () {
    if (!('randomUUID' in crypto)) {
        // https://stackoverflow.com/a/2117523/2800218
        // LICENSE: https://creativecommons.org/licenses/by-sa/4.0/legalcode
        crypto.randomUUID = function randomUUID() {
            return (
                [1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,
                c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
            );
        };

    }

    return crypto.randomUUID().replaceAll('-', '_')
}
globalThis._createUUID = createUUID;

export const css = (strings, ...values) => String.raw({ raw: strings }, ...values);

export function travelChildren(children = [], opt = {}) {
    [...children].forEach(child => {
        if (opt && opt.handle) {
            opt.handle(child);
        }
        if (child.children && child.children.length) {
            travelChildren(child.children, opt);
        }
    })
}

export function setBoolAttr(target, name, value) {
    if (typeof value === 'undefined' || value === null) {
        target.removeAttribute(name);
    }
    else {
        try {
            target?.setAttribute(name, typeof value === 'boolean' ? true : value);
        } catch (e) {
            console.log(target)
        }
    }
}


/**
 *
 * @param nodes
 * @param nodeDesc
 * @returns {{eventLists: *[]}}
 */
export function travelChildNodes(nodes, nodeDesc = {eventLists: []}) {


    [...nodes].forEach((node) => {
        // console.dir(node)
        if (node.nodeName === "#text") {
        //
        }
        else {
            if (node instanceof HTMLElement) {

                let isPushed = false;
                let keys =Object.keys(node.attributes)
                keys.forEach(key => {
                    /**
                     * @type {Attr}
                     */
                    let attr = node.attributes[key];
                    let value = attr.nodeValue;
                    if (attr.name.startsWith("bind:")) {
                        let events = attr.name.split(":");
                        let eventName = events[events.length - 1];

                        nodeDesc.eventLists.push({
                            eventName: eventName,
                            methodName: value,
                            element: node
                        })
                    }

                    if (!isPushed && attr.name.startsWith(":")) {
                        isPushed = true;
                        nodeDesc.bindcLists.push({
                            element: node
                        });
                    }

                    if (nodeDesc.onTravel){
                        nodeDesc.onTravel(attr.name, value, node);
                    }


                    // console.log(attr.name, value)
                });
                if (keys.length < 1) {
                    if (nodeDesc.onTravel){
                        nodeDesc.onTravel('', void  0, node);
                    }
                }
                if (node.childNodes) {
                    travelChildNodes(node.childNodes, nodeDesc);
                }
            }
        }
    });

    return nodeDesc
}

/**
 * @typedef bindRootRet
 * @prop {function} unbindEvents
 */

/**
 *
 * @typedef funcItemType
 * @prop {string} eventName
 * @prop  {Element} element
 * @prop {function} function
 */

/**
 *
 * @param shadowRoot {Element}
 * @param methods {{}}
 * @param ctx {{}}
 * @return {bindRootRet}
 */
export function bindRootEle(shadowRoot,methods = {}, ctx) {
    let bindEvents = new CondsMap()
    let _currentEventTarget = null;

    let nodeDesc = {
        eventLists: [],
        bindcLists: [],
        onTravel(attrname, value, node) {
            if(ctx.onTravel){
                ctx.onTravel(attrname, value, node);
            }
        }
    };

    travelChildNodes(shadowRoot.childNodes, nodeDesc);
    // console.log(nodeDesc)

    // console.log( nodeDesc.bindcLists)

    nodeDesc.bindcLists.forEach((e) => {
        let element = e.element;
        if (ctx._onBindRootEle) {
            ctx._onBindRootEle(element);
        }
    })

    nodeDesc.eventLists.forEach((e) => {
        let eventName = e.eventName;
        let methodName = e.methodName;
        let element = e.element;
        if (methods[methodName]) {
            if (!element.isAddListener) {
                // console.log("isAddListener", element)
                let fun = function (e) {
                    let target = e.currentTarget || e.target;
                    _currentEventTarget = target;
                    methods[methodName].bind({
                        ...ctx?.exportCtx(),
                        get $target() {
                            return target
                        },
                    })(e)
                }


                /**
                 *
                 * @type {funcItemType}
                 */
                let item = {
                    eventName,
                    element: element,
                    function: fun,
                }
                bindEvents.set(eventName, item);
                element.addEventListener(eventName, fun);
                element.isAddListener = true;
            }
        }
    });


    return {
        unbindEvents: () => {
            bindEvents.forEach(function (arr = [], key) {
                /**
                 * @type {[funcItemType]}
                 */
                let items = [].concat(arr);
                items.forEach(item => {
                    item.element.removeEventListener(item.eventName, item.function)
                })
            })
        },
        checkIsCurrentEventTarget(target) {
            return _currentEventTarget === target;
        }
    }

}

export function getNotSupportClsArr() {
    let clsArr = [];
    if (!("anchorName" in document.documentElement.style)) {
        // document.body.append("This browser does not support HTMLAnchor.");
        clsArr.push('not-support-anchor')
    }
    return clsArr;
}

function capitalizedStr(word = '') {
    return word.charAt(0).toUpperCase()
    + word.slice(1)
}

/**
 *
 * @param dom {Element}
 * @param type {'width'|'height'}
 */
export function getDomSize(dom, {type = 'width'} = {}) {
    return dom.getBoundingClientRect()[type]
}

/**
 *
 * @param dom {Node}
 * @param ctx {{}}
 */
function travelZConfig(dom, ctx = {}) {
    let configSelName = 'z-config';
    if (!dom.closest && dom.host) {
        dom = dom.host;
        // console.dir(dom)
    }
    if (dom.$configEle) {
        ctx.ele = dom.$configEle
    }
    else {
        if (  dom.closest) {
            let ele =  dom.closest(configSelName);
            if (!ele) {
                if (dom && dom.parentNode) {
                    travelZConfig(dom.parentNode, ctx);
                }
            } else {
                ctx.ele = ele
            }
        } else {

            console.dir(dom)
        }
    }
}

function getScrollParent(element, includeHidden) {
    let style = getComputedStyle(element);
    let excludeStaticParent = style.position === "absolute";
    let overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;

    if (style.position === "fixed") return document.body;
    for (var parent = element; (parent = parent.parentElement);) {
        style = getComputedStyle(parent);
        if (excludeStaticParent && style.position === "static") {
            continue;
        }
        if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent;
    }

    return document.body;
}
globalThis._getScrollParent = getScrollParent;

// function getScrollParent(node) {
//     if (node == null) {
//         return null;
//     }
//
//     let stylemap = node.computedStyleMap();
//     // console.log('stylemap.get(\'overflow\')', stylemap.get('overflow').toString())
//     if (node.scrollHeight > node.clientHeight && stylemap.get('overflow').toString() !=='visible') {
//         return node;
//     } else {
//         return getScrollParent(node.parentNode);
//     }
// }

export class BaseEle extends HTMLElement {
    constructor() {
        super();
        let self = this;
        const supportsDeclarative = HTMLElement.prototype.hasOwnProperty("attachInternals");
        const internals = supportsDeclarative ? this.attachInternals() : undefined;

        self.$configEle = null;
        let findCfx = {
            ele: null,
        }

        travelZConfig(self, findCfx);
        self.$configEle = findCfx.ele;
        // if (!self.$configEle) {
        //     console.log(self, self.$configEle);
        // }

        self._locks = false;

        /**
         *
         * @type {{addState(string=): void, hasState(string=): boolean, deleteState(string=): void}}
         */
        let extendState = {
            addState(name = '') {
                internals.states.add(name);
                updateState()
            },
            hasState(name = '') {
                return internals.states.has(name);
            },
            deleteState(name = '') {
                internals.states.delete(name);
                updateState()
            }
        }

        /**
         *
         * @type {ElementInternals & extendState}
         * @private
         */
        this._internals = Object.assign(internals,extendState);

        function updateState() {
            // console.log( internals.states);
            if (self.onStateUpdate) {
                self.onStateUpdate( internals.states)
            }
        }

        this.stylesheet = new CSSStyleSheet();
        this.stylesheet.__id__ = createUUID()
        this._log = console.log.bind(console);
    }

    _uuid() {
        return createUUID()
    }

    _getScrollParent() {
        return getScrollParent(this);
    }

    getDomById(v = '') {
        let shadowRoot =  this.shadowRoot ? this.shadowRoot : this._shadowRoot
        return shadowRoot.querySelector('#' +v)
    }

    setLock(time) {
        this._locks = true;
        setTimeout(() => {
            this._locks = false;
        }, time)
    }

    _createInterLock(name) {
        let intername = '__inter_'+ name;
        this[name] = false;
        return {
            get locked() {
               return this[name]
            },
            run() {
                clearInterval( this[intername]);
                this[name] = true;
                this[intername] = setTimeout(() => {
                    this[name] = false;
                }, 100)
            }
        }
    }

    setCSS(css = '', dom = this.shadowRoot) {
        this.stylesheet.replaceSync(css);
        // dom.adoptedStyleSheets = [this.stylesheet]

        let hasFindCSS = false;
        dom.adoptedStyleSheets.forEach((styleSheet, index) => {
            if (styleSheet.__id__ === this.stylesheet.__id__) {
                hasFindCSS = true;
                this.stylesheet.replaceSync(css);
                dom.adoptedStyleSheets[index] = this.stylesheet
            }
        });
        if (!hasFindCSS) {
            dom.adoptedStyleSheets.push(this.stylesheet)
        }
        this._log('dom.adoptedStyleSheets', dom.adoptedStyleSheets.filter(styleSheet => {
            return styleSheet.__id__ === this.stylesheet.__id__
        }))
    }

    setTemplate(sel = '') {
        let template = document.querySelector(sel);
        let templateContent = template.content;
        let content = templateContent.cloneNode(true);

        const shadowRoot = this.attachShadow({ mode: "open" });
        shadowRoot.appendChild(content);

        return shadowRoot
    }

    static defsel(name = '', options = {}) {
        elementMeta[this.name] = {
            options
        }

        if (options?.attrs) {
            this.defAttrs(options.attrs)
        }
        customElements.define(name, this)
    }
    
    static defAttrs(def ={}) {
        let observedAttributes =  this.observedAttributes ?? [];

        this.observedAttributes = observedAttributes.concat(Object.keys(def));
        // console.log('observedAttributes', this, this.observedAttributes);
    }

    _slotchange(e) {
        // console.log('_slotchange', e, this.host);
        let slots = [...this.querySelectorAll('slot')];
        let slotnodes =  slots.map(v => {
            return v.assignedNodes()
        })
        let hasSloted = slots.some(v => {
            return v.assignedNodes()?.length > 0
        });
        // console.log(hasSloted);
        this.host.classList.toggle('_has-sloted', hasSloted);

        // console.log(this, this.host)
        if (this.host.onSlotChange) {
            this.host.onSlotChange(e, {slotnodes: slotnodes})
        }
    }

    connectedCallback() {
        // console.log("Custom element added to page.");
        this.shadowRoot?.addEventListener("slotchange",  this._slotchange);
        if (this.mounted) {
            this.mounted()
        }
    }

    disconnectedCallback() {
        console.log("Custom element removed from page.", this);
        this.shadowRoot?.removeEventListener("slotchange",  this._slotchange);
        if (this.unmounted) {
            this.unmounted()
        }
    }

    adoptedCallback() {
        // console.log("Custom element moved to new page.");
    }

    _getAttr(name) {
        let b =  getMeta(this);
        let attrValue = this.getAttribute(name);
        console.log('attrValue', attrValue)
        if (typeof attrValue === 'undefined' || attrValue === null) {
            if (b?.options?.attrs) {
               return b?.options?.attrs[name]
            }
        }
        return attrValue;
    }

    attributeChangedCallback(name, oldValue, newValue) {
        // console.log(this.constructor.name, getMeta(this))
        let b =  getMeta(this)
        if (b?.options?.attrs) {
            let initval = b?.options?.attrs[name]
            // console.log(name, initval);
            if (typeof(oldValue) === 'undefined') {
                oldValue = initval
            }
            if(this.attrChanged) {
                this.attrChanged(name, oldValue, newValue)
            }
        }
        // console.log(`Attribute ${name} has changed.${newValue}`);
    }

    attrChanged(name, oldValue, newValue) {
        console.log(`Attribute ${name} has changed ${oldValue} ${newValue}`);
    }
}
customElements.define('z-base-ele', BaseEle)

function filterNone() {
    return NodeFilter.FILTER_ACCEPT;
}

/**
 *
 * @param rootElem {HTMLElement}
 * @returns {Node[]}
 */
function getAllComments(rootElem) {
    /**
     *
     * @type {Node[]}
     */
    let comments = [];
    // Fourth argument, which is actually obsolete according to the DOM4 standard, is required in IE 11
    let iterator = document.createNodeIterator(rootElem, NodeFilter.SHOW_COMMENT, filterNone, false);
    let curNode;
    while (curNode = iterator.nextNode()) {
        comments.push(curNode);
    }
    return comments;
}

/**
 *
 * @type {{getAllComments: (function(HTMLElement): Node[])}}
 */
export let utils = {
    getAllComments
}